# -*- coding: utf-8 -*-
import itertools

# conda install -c conda-forge scikit-surprise
from surprise import accuracy
from collections import defaultdict


# 推荐指标类
class RecommenderMetrics:

    def MAE(predictions) -> float:
        """
        计算预测值的平均绝对误差（Mean Absolute Error, MAE）。

        :param predictions: 预测结果列表。
        :return: MAE 值。
        """
        return accuracy.mae(predictions, verbose=False)

    def RMSE(predictions) -> float:
        """
        计算预测值的均方根误差（Root Mean Square Error, RMSE）。

        :param predictions: 预测结果列表。
        :return: RMSE 值。
        """
        return accuracy.rmse(predictions, verbose=False)

    def GetTopN(predictions, n: int = 10, minimumRating: float = 4.0) -> dict:
        """
        获取每个用户的 Top-N 推荐列表。

        :param predictions: 预测结果列表。
        :param n: 推荐列表的长度，默认为 10。
        :param minimumRating: 最低评分阈值，默认为 4.0。
        :return: 每个用户的 Top-N 推荐列表字典。
        """
        topN = defaultdict(list)

        for userID, movieID, actualRating, estimatedRating, _ in predictions:
            if (estimatedRating >= minimumRating):
                topN[int(userID)].append((int(movieID), estimatedRating))

        for userID, ratings in topN.items():
            ratings.sort(key=lambda x: x[1], reverse=True)
            topN[int(userID)] = ratings[:n]

        return topN

    def HitRate(topNPredicted: dict, leftOutPredictions) -> float:
        """
        计算命中率（Hit Rate）。

        :param topNPredicted: 每个用户的 Top-N 推荐列表字典。
        :param leftOutPredictions: 留出的预测结果列表。
        :return: 命中率。
        """
        hits = 0
        total = 0

        for leftOut in leftOutPredictions:
            userID = leftOut[0]
            leftOutMovieID = leftOut[1]
            # 该leftout是否是topN里的
            hit = False
            for movieID, predictedRating in topNPredicted[int(userID)]:
                if (int(leftOutMovieID) == int(movieID)):
                    hit = True
                    break
            if (hit):
                hits += 1

            total += 1

        # 计算命中比例
        return hits / total if total != 0 else 0.0

    def CumulativeHitRate(topNPredicted: dict, leftOutPredictions, ratingCutoff: float = 0) -> float:
        """
        计算阈值命中率（Cumulative Hit Rate）。

        :param topNPredicted: 每个用户的 Top-N 推荐列表字典。
        :param leftOutPredictions: 留出的预测结果列表。
        :param ratingCutoff: 评分阈值，默认为 0。
        :return: 阈值命中率。
        """
        hits = 0
        total = 0

        for userID, leftOutMovieID, actualRating, estimatedRating, _ in leftOutPredictions:
            # 如果实际评分达到阈值
            if (actualRating >= ratingCutoff):
                hit = False
                # 检测是否命中
                for movieID, predictedRating in topNPredicted[int(userID)]:
                    if (int(leftOutMovieID) == int(movieID)):
                        hit = True
                        break
                if (hit):
                    hits += 1

                total += 1

        return hits / total if total != 0 else 0.0

    def RatingHitRate(topNPredicted: dict, leftOutPredictions) -> dict:
        """
        计算按评分划分的命中率（Rating Hit Rate）。

        :param topNPredicted: 每个用户的 Top-N 推荐列表字典。
        :param leftOutPredictions: 留出的预测结果列表。
        :return: 按评分划分的命中率字典。
        """
        hits = defaultdict(float)
        total = defaultdict(float)

        for userID, leftOutMovieID, actualRating, estimatedRating, _ in leftOutPredictions:
            hit = False
            for movieID, predictedRating in topNPredicted[int(userID)]:
                if (int(leftOutMovieID) == int(movieID)):
                    hit = True
                    break
            if (hit):
                # 该评分级别命中数+1
                hits[actualRating] += 1

            total[actualRating] += 1

        result = {}
        for rating in sorted(hits.keys()):
            # 该评分级别命中率 = 该评分级别命中数 / 该评分级别总数
            result[rating] = hits[rating] / total[rating] if total[rating] != 0 else 0.0

        return result

    def AverageReciprocalHitRank(topNPredicted: dict, leftOutPredictions) -> float:
        """
        计算平均倒数命中排名（Average Reciprocal Hit Rank）。

        :param topNPredicted: 每个用户的 Top-N 推荐列表字典。
        :param leftOutPredictions: 留出的预测结果列表。
        :return: 平均倒数命中排名。
        """
        summation = 0
        total = 0

        for userID, leftOutMovieID, actualRating, estimatedRating, _ in leftOutPredictions:
            hitRank = 0
            rank = 0
            for movieID, predictedRating in topNPredicted[int(userID)]:
                rank += 1
                if (int(leftOutMovieID) == movieID):
                    hitRank = rank
                    break
            if (hitRank > 0):
                summation += 1.0 / hitRank

            total += 1

        return summation / total if total != 0 else 0.0

    def UserCoverage(topNPredicted: dict, numUsers: int, ratingThreshold: float = 0) -> float:
        """
        计算用户覆盖率（User Coverage）。

        :param topNPredicted: 每个用户的 Top-N 推荐列表字典。
        :param numUsers: 用户总数。
        :param ratingThreshold: 评分阈值，默认为 0。
        :return: 用户覆盖率。
        """
        hits = 0
        for userID in topNPredicted.keys():
            hit = False
            for movieID, predictedRating in topNPredicted[userID]:
                if (predictedRating >= ratingThreshold):
                    hit = True
                    break
            if (hit):
                hits += 1

        return hits / numUsers if numUsers != 0 else 0.0

    def Diversity(topNPredicted: dict, simsAlgo) -> float:
        """
        计算推荐结果的多样性（Diversity）。

        :param topNPredicted: 每个用户的 Top-N 推荐列表字典。
        :param simsAlgo: 相似度算法对象。
        :return: 多样性值。
        """
        n = 0
        total = 0
        # 相似度矩阵
        simsMatrix = simsAlgo.compute_similarities()
        for userID in topNPredicted.keys():
            # 生成所有可能的项目对
            pairs = itertools.combinations(topNPredicted[userID], 2)
            for pair in pairs:
                movie1 = pair[0][0]
                movie2 = pair[1][0]
                innerID1 = simsAlgo.trainset.to_inner_iid(str(movie1))
                innerID2 = simsAlgo.trainset.to_inner_iid(str(movie2))
                # 根据moiveID得到相似度
                similarity = simsMatrix[innerID1][innerID2]
                # 相似度累加
                total += similarity
                # 总数累加
                n += 1

        # 平均相似度
        S = total / n if n != 0 else 0.0
        return (1 - S)

    def Novelty(topNPredicted: dict, rankings: dict) -> float:
        """
        计算推荐结果的新颖性（Novelty）。

        :param topNPredicted: 每个用户的 Top-N 推荐列表字典。
        :param rankings: 项目的流行度排名信息。
        :return: 新颖性值。
        """
        n = 0
        total = 0
        for userID in topNPredicted.keys():
            for rating in topNPredicted[userID]:
                # print(rating ,rankings)
                movieID = rating[0]
                rank = rankings[movieID]
                # 评分累加
                total += rank
                n += 1
        return total / n if n != 0 else 0.0
